package au.edu.qut.bpm.genbinder.genetic.mutation;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import org.uncommons.maths.number.ConstantGenerator;
import org.uncommons.maths.number.NumberGenerator;
import org.uncommons.watchmaker.framework.EvolutionaryOperator;

import au.edu.qut.bpm.genbinder.models.Activity;
import au.edu.qut.bpm.genbinder.models.Case;
import au.edu.qut.bpm.genbinder.models.ConcreteEventLog;
import au.edu.qut.bpm.genbinder.models.Resource;

public class LogTimeShiftMutation implements EvolutionaryOperator<ConcreteEventLog> {
	private boolean performSafetyCheck = true;
	double delta = 1d;
	
	private long minutesInDay = 1440;
	private long workingHours = 480;
	private long nonWorkingHours = minutesInDay - workingHours;

	private final NumberGenerator<Integer> mutationCaseCountVariable;
	private final NumberGenerator<Integer> mutationActivityCountVariable;

	public LogTimeShiftMutation(boolean performSafetyCheck) {
		this(performSafetyCheck, 3, 3);
	}

	public LogTimeShiftMutation(boolean performSafetyCheck, int mutationCaseCount, int mutationActivityCount) {
		this(performSafetyCheck, new ConstantGenerator<Integer>(mutationCaseCount),	new ConstantGenerator<Integer>(mutationActivityCount));
	}

	public LogTimeShiftMutation(boolean performSafetyCheck,	NumberGenerator<Integer> mutationCaseCount,	NumberGenerator<Integer> mutationActivityCount) {
		this.performSafetyCheck = performSafetyCheck;
		this.mutationCaseCountVariable = mutationCaseCount;
		this.mutationActivityCountVariable = mutationActivityCount;
		
	}

	public List<ConcreteEventLog> apply(List<ConcreteEventLog> selectedCandidates, Random rng) {
		List<ConcreteEventLog> mutatedCandidates = new ArrayList<ConcreteEventLog>(selectedCandidates.size());
		for (ConcreteEventLog log : selectedCandidates) {
			//mutatedCandidates.add(mutate(log, rng));
			mutatedCandidates.add(mutate(log, rng));
		}
		return mutatedCandidates;
	}

	private ConcreteEventLog mutate (ConcreteEventLog log, Random rng) {
		ConcreteEventLog newLog = log.clone();
		
		int mutationCaseCount = Math.abs(mutationCaseCountVariable.nextValue());
		while (mutationCaseCount > 0) {
			int mutationActivityCount = Math.abs(mutationActivityCountVariable.nextValue());
			int c = rng.nextInt(newLog.getAbstractLog().getCases().size());
			Case cas = newLog.getAbstractLog().getCases().get(c);

			while (mutationActivityCount > 0) {
				List<Activity> trace = newLog.getSortedTrace(cas);
				int a = rng.nextInt(trace.size());
				Activity act = trace.get(a);

				long activityPossibleStartMin = (a == 0) ? cas.getArrival()	: newLog.getActivityEnd(trace.get(a - 1));
				//long activityPossibleStartMax = newLog.getActivityStart(act);
				
				long activityPossibleStartMax = 
						(a == (trace.size() - 1)) ? (long) (newLog.getCaseEnd(cas) + act.getDuration())	: newLog.getActivityStart(trace.get(a + 1)) - act.getDuration();

				long diff = activityPossibleStartMax - activityPossibleStartMin;
				
				long newStart = (diff <= 0)	? activityPossibleStartMin : activityPossibleStartMin + (long) rng.nextInt((int) (diff));

//				System.out.println(act.getId() + " = " + newStart + " = test = " + newStart%minutesInDay + " = " + ((newStart%minutesInDay) < workingHours));
				
				//TODO 1: check newstart within working period?
				if ( (newStart%minutesInDay) > workingHours) {
					newStart += (nonWorkingHours - ( (newStart%minutesInDay) - workingHours) );
//					System.out.println("newstart = " + newStart);
				}
				
				
				long actualDur = act.getDuration();
//				System.out.println("actualDur = " + actualDur);
				if (actualDur > 480) {
					actualDur = actualDur - 960;
				}
				
				long newEnd = newStart + actualDur;

				//TODO 2; check newEnd within working period...
				if ( (newEnd%minutesInDay) > workingHours) {
					newEnd = RecursiveNewEndTime(newEnd);
				}

				if (performSafetyCheck) {
					boolean problem = false;

					//check whether there is an activity within the proposed timeframe, within a case, for the new log
					for (Activity a2 : cas.getActivities()) {
						if (newLog.isActivityWithin(a2, newStart, newEnd)){
							//TODO check here...
							problem = true;
							break;
						}
					}

					//Set<Resource> usedRes = newLog.getResourcesUtilizedWithin(newStart, newStart + act.getDuration(), cas);
					if (!problem){
						for (Resource r : newLog.getResourcesUtilizedWithin(newStart, newEnd, cas)) {
							//TODO a function to check the actual working duration
							if (newLog.getActivityResources(act).contains(r)) {
								problem = true;
								break;
							}
						}
					}
					
					if (!problem) {
						act.setDuration(newEnd - newStart);
						newLog.setActivityStart(act, newStart);
//						newLog.setActivityEnd(act, newEnd);
						//TODO need to set new activity duration/end time as well...
					}
						
						
				} else {
//					act.setDuration(newEnd - newStart);
					newLog.setActivityStart(act, newStart);
//					newLog.setActivityEnd(act, newEnd);
				}
					
					
				/*} else {
					long activityPossibleStartMin = (long) (newLog.getActivityStart(act) - (double) act.getDuration() * delta);
					long activityPossibleStartMax = (long) (newLog.getActivityStart(act) + (double) act.getDuration() * delta);
					if (activityPossibleStartMin <= newLog.getCaseArrival(cas))
						activityPossibleStartMin = newLog.getCaseArrival(cas);
					long newStart = activityPossibleStartMin + (long) rng.nextInt((int) (activityPossibleStartMax - activityPossibleStartMin));
					newLog.setActivityStart(act, newStart);
				}*/
				--mutationActivityCount;
			}
			--mutationCaseCount;
		}

		return newLog;
	}
	
	public long RecursiveNewEndTime(long newEnd) {
//		System.out.println("oldend = " + newEnd);
		newEnd += nonWorkingHours;
//		System.out.println("newend = " + newEnd);
		
		if ((newEnd%minutesInDay) > workingHours) {
//			System.out.println("recursive");
			return RecursiveNewEndTime(newEnd);
		}

		return newEnd;
	}
	
	/*
	private ConcreteEventLog mutate(ConcreteEventLog log, Random rng) {
		ConcreteEventLog newLog = log.clone();

		int mutationCaseCount = Math.abs(mutationCaseCountVariable.nextValue());
		while (mutationCaseCount > 0) {
			int mutationActivityCount = Math.abs(mutationActivityCountVariable.nextValue());
			int c = rng.nextInt(newLog.getAbstractLog().getCases().size());
			Case cas = newLog.getAbstractLog().getCases().get(c);

			while (mutationActivityCount > 0) {
				List<Activity> trace = newLog.getSortedTrace(cas);
				int a = rng.nextInt(trace.size());
				Activity act = trace.get(a);

				if (performSafetyCheck) {
					long activityPossibleStartMin = 
							(a == 0) ? cas.getArrival()	: newLog.getActivityEnd(trace.get(a - 1));
					long activityPossibleStartMax = 
							(a == (trace.size() - 1)) ? (long) (newLog.getCaseEnd(cas) + act.getDuration() * delta)	: newLog.getActivityStart(trace.get(a + 1)) - act.getDuration();
					
					long diff = activityPossibleStartMax - activityPossibleStartMin;
					
					long newStart = 
							(diff <= 0)	? activityPossibleStartMin : activityPossibleStartMin + (long) rng.nextInt((int) (diff));

					Set<Resource> usedRes = newLog.getResourcesUtilizedWithin(newStart, newStart + act.getDuration(), cas);
					boolean problem = false;
					
					for (Resource r : usedRes) {
						if (newLog.getActivityResources(act).contains(r)) {
							problem = true;
							break;
						}
					}
					if (!problem)
						newLog.setActivityStart(act, newStart);
				} else {
					long activityPossibleStartMin = (long) (newLog.getActivityStart(act) - (double) act.getDuration() * delta);
					long activityPossibleStartMax = (long) (newLog.getActivityStart(act) + (double) act.getDuration() * delta);
					if (activityPossibleStartMin <= newLog.getCaseArrival(cas))
						activityPossibleStartMin = newLog.getCaseArrival(cas);
					long newStart = activityPossibleStartMin + (long) rng.nextInt((int) (activityPossibleStartMax - activityPossibleStartMin));
					newLog.setActivityStart(act, newStart);
				}
				--mutationActivityCount;
			}
			--mutationCaseCount;
		}
		//Date date2 = new Date();
		//System.err.println("TimeMutEnd: " + date2.toString());
		return newLog;
	}
	*/
}
